C# defines a good number of query operators out of the box. Table 13-3 documents some of the more commonly used query operators.
Query Operators | Meaning in Life |
---|---|
from, in | Used to define the backbone for any LINQ expression, which allows you to extract a subset of data from a fitting container. |
where | Used to define a restriction for which items to extract from a container. |
select | Used to select a sequence from the container. |
join, on, equals, into | Performs joins based on specified key. Remember, these “joins” do not need to have anything to do with data in a relational database. |
orderby, ascending, descending | Allows the resulting subset to be ordered in ascending or descending order. |
group, by | Yields a subset with data grouped by a specified value. |
In addition to the partial list of operators shown in Table 13-3, the System.Linq.Enumerable class provides a set of methods that do not have a direct C# query operator shorthand notation, but are instead exposed as extension methods. These generic methods can be called to transform a result set in various manners (Reverse<>(), ToArray<>(), ToList<>(), etc.). Some are used to extract singletons from a result set, others perform various set operations (Distinct<>(), Union<>(), Intersect<>(), etc.), and still others aggregate results (Count<>(), Sum<>(), Min<>(), Max<>(), etc.).
To begin digging into more intricate LINQ queries, create a new Console Application named FunWithLinqExpressions. Next, you need to define an array or collection of some sample data. For this project, you will make an array of ProductInfo objects, defined in the following code:
class ProductInfo { public string Name {get; set;} public string Description {get; set;} public int NumberInStock {get; set;} public override string ToString() { return string.Format("Name={0}, Description={1}, Number in Stock={2}", Name, Description, NumberInStock); } }
Now populate an array with the a batch of ProductInfo objects within your Main() method:
static void Main(string[] args) { Console.WriteLine("***** Fun with Query Expressions *****\n"); // This array will be the basis of our testing... ProductInfo[] itemsInStock = new[] { new ProductInfo{ Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberInStock = 24}, new ProductInfo{ Name = "Milk Maid Milk", Description = "Milk cow's love", NumberInStock = 100}, new ProductInfo{ Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberInStock = 120}, new ProductInfo{ Name = "Cruchy Pops", Description = "Cheezy, peppery goodness", NumberInStock = 2}, new ProductInfo{ Name = "RipOff Water", Description = "From the tap to your wallet", NumberInStock = 100}, new ProductInfo{ Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberInStock = 73} }; // We will call various methods here! Console.ReadLine(); }
Because the syntactical correctness of a LINQ query expression is validated at compile time, you need to remember that the ordering of these operators is critical. In the simplest terms, every LINQ query expression is built using the from, in, and select operators. Here is the general template to follow:
var result = from matchingItem in container select matchingItem;
The item after the from operator represents an item that matches the LINQ query criteria, which can be named anything you choose. The item after the in operator, represents the data container to search (an array, collection, or XML document).
Here is a very simple query, doing nothing more than selecting every item in the container (similar in behavior to a database Select * SQL statement). Consider the following:
static void SelectEverything(ProductInfo[] products) { // Get everything! Console.WriteLine("All product details:"); var allProducts = from p in products select p; foreach (var prod in allProducts) { Console.WriteLine(prod.ToString()); } }
To be honest, this query expression is not entirely useful, given that your subset is identical to that of the data in the incoming parameter. If you wish, you could use this incoming parameter to extract only the Name values of each car using the following selection syntax:
static void ListProductNames(ProductInfo[] products) { // Now get only the names of the products. Console.WriteLine("Only product names:"); var names = from p in products select p.Name; foreach (var n in names) { Console.WriteLine("Name: {0}", n); } }
To obtain a specific subset from a container, you can make use of the where operator. When doing so, the general template now becomes the following code:
var result = from item in container where BooleanExpression select item;
Notice that the where operator expects an expression that resolves to a Boolean. For example, to extract from the ProductInfo[] argument only the items that have more than 25 items on hand, you could author the following code:
static void GetOverstock(ProductInfo[] products) { Console.WriteLine("The overstock items!"); // Get only the items where we have more than // 25 in stock. var overstock = from p in products where p.NumberInStock > 25 select p; foreach (ProductInfo c in overstock) { Console.WriteLine(c.ToString()); } }
As seen earlier in this chapter, when you are building a where clause, it is permissible to make use of any valid C# operators to build complex expressions. For example, recall the query that only extracts out the BMWs going at least 100 mph:
// Get BMWs going at least 100 mph. var onlyFastBMWs = from c in myCars where c.Make == "BMW" && c.Speed >= 100 select c; foreach (Car c in onlyFastBMWs) { Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed); }
It is also possible to project new forms of data from an existing data source. Let’s assume that you wish to take the incoming ProductInfo[] parameter and obtain a result set that accounts only for the name and description of each item. To do so, you can define a select statement that dynamically yields a new anonymous type:
static void GetNamesAndDescriptions(ProductInfo[] products) { Console.WriteLine("Names and Descriptions:"); var nameDesc = from p in products select new { p.Name, p.Description }; foreach (var item in nameDesc) { // Could also use Name and Description properties directly. Console.WriteLine(item.ToString()); } }
Always remember that when you have a LINQ query that makes use of a projection, you have no way of knowing the underlying data type, as this is determined at compile time. In these cases, the var keyword is mandatory. As well, recall that you cannot create methods with implicitly typed return values. Therefore, the following method would not compile:
static var GetProjectedSubset() { ProductInfo[] products = new[] { new ProductInfo{ Name = "Mac's Coffee", Description = "Coffee with TEETH", NumberInStock = 24}, new ProductInfo{ Name = "Milk Maid Milk", Description = "Milk cow's love", NumberInStock = 100}, new ProductInfo{ Name = "Pure Silk Tofu", Description = "Bland as Possible", NumberInStock = 120}, new ProductInfo{ Name = "Cruchy Pops", Description = "Cheezy, peppery goodness", NumberInStock = 2}, new ProductInfo{ Name = "RipOff Water", Description = "From the tap to your wallet", NumberInStock = 100}, new ProductInfo{ Name = "Classic Valpo Pizza", Description = "Everyone loves pizza!", NumberInStock = 73} }; var nameDesc = from p in products select new { p.Name, p.Description }; return nameDesc; // Nope! }
When you wish to return projected data to a caller, one approach is to transform the query result into a .NET System.Array object using the ToArray() extension method. Thus, if you were to update your query expression as follows:
// Return value is now an Array. static Array GetProjectedSubset() { ... // Map set of anonymous objects to an Array object. return nameDesc.ToArray(); }
you could invoke and process the data from Main() as follows:
Array objs = GetProjectedSubset(); foreach (object o in objs) { Console.WriteLine(o); // Calls ToString() on each anonymous object. }
Note that you have to use a literal System.Array object and cannot make use of the C# array declaration syntax, given that you don’t know the underlying type of type, as you are operating on a compiler generated anonymous class! Also note that you are not specifying the type parameter to the generic ToArray<T>() method, as you once again don’t know the underlying data type until compile time, which is too late for your purposes.
The obvious problem is that you lose any strong typing, as each item in the Array object is assumed to be of type Object. Nevertheless, when you need to return a LINQ result set which is the result of a projection operation, transforming the data into an Array type (or another suitable container via other members of the Enumerable type) is mandatory.
When you are projecting new batches of data, you may need to discover exactly how many items have been returned into the sequence. Any time you need to determine the number of items returned from a LINQ query expression, simply make use of the Count() extension method of the Enumerable class. For example, the following method will find all string objects in a local array which have a length greater than six characters:
static void GetCountFromQuery() { string[] currentVideoGames = {"Morrowind", "Uncharted 2", "Fallout 3", "Daxter", "System Shock 2"}; // Get count from the query. int numb = (from g in currentVideoGames where g.Length > 6 select g).Count<string>(); // Print out the number of items. Console.WriteLine("{0} items honor the LINQ query.", numb); }
You can reverse the items within a result set quite simply using the Reverse<T>() extension method of the Enumerable class. For example, the following method selects all items from the incoming ProductInfo[] parameter in reverse:
static void ReverseEverything(ProductInfo[] products) { Console.WriteLine("Product in reverse:"); var allProducts = from p in products select p; foreach (var prod in allProducts.Reverse()) { Console.WriteLine(prod.ToString()); } }
As you have seen over this chapter’s initial examples, a query expression can take an orderby operator to sort items in the subset by a specific value. By default, the order will be ascending; thus, ordering by a string would be alphabetical, ordering by numerical data would be lowest to highest, and so forth. If you wish to view the results in a descending order, simply include the descending operator. Ponder the following method:
static void AlphabetizeProductNames(ProductInfo[] products) { // Get names of products, alphabetized. var subset = from p in products orderby p.Name select p; Console.WriteLine("Ordered by Name:"); foreach (var p in subset) { Console.WriteLine(p.ToString()); } }
Although ascending order is the default, you are able to make your intentions very clear by making use of the ascending operator:
var subset = from p in products orderby p.Name ascending select p;
If you wish to get the items in descending order, you can do so via the descending operator:
var subset = from p in products orderby p.Name descending select p;
The Enumerable class supports a set of extension methods which allows you to use two (or more) LINQ queries as the basis to find unions, differences, concatenations, and intersections of data. First of all, consider the Except() extension method, which will return a LINQ result set that contains the differences between two containers, which in this case, is the value "Yugo":
static void DisplayDiff() { List<string> myCars = new List<String> {"Yugo", "Aztec", "BMW"}; List<string> yourCars = new List<String>{"BMW", "Saab", "Aztec" }; var carDiff =(from c in myCars select c) .Except(from c2 in yourCars select c2); Console.WriteLine("Here is what you don't have, but I do:"); foreach (string s in carDiff) Console.WriteLine(s); // Prints Yugo. }
The Intersect() method will return a result set that contains the common data items in a set of containers. For example, the following method returns the sequence, “Aztec” and “BMW”.
static void DisplayIntersection() { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; // Get the common members. var carIntersect = (from c in myCars select c) .Intersect(from c2 in yourCars select c2); Console.WriteLine("Here is what we have in common:"); foreach (string s in carIntersect) Console.WriteLine(s); // Prints Aztec and BMW. }
The Union() method, as you would guess, returns a result set that includes all members of a batch of LINQ queries. Like any proper union, you will not find repeating values if a common member appears more than once. Therefore, the following method will print out the values “Yugo”, “Aztec”, “BMW”, and "Saab":
static void DisplayUnion() { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; // Get the union of these containers. var carUnion = (from c in myCars select c) .Union(from c2 in yourCars select c2); Console.WriteLine("Here is everything:"); foreach (string s in carUnion) Console.WriteLine(s); // Prints all common members }
Finally, the Concat()extension method returns a result set that is a direct concatenation of LINQ result sets. For example, the following method prints out the results “Yugo”, “Aztec”, “BMW”, “BMW”, “Saab”, and “Aztec”:
static void DisplayConcat() { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; var carConcat = (from c in myCars select c) .Concat(from c2 in yourCars select c2); // Prints: // Yugo Aztec BMW BMW Saab Aztec. foreach (string s in carConcat) Console.WriteLine(s); }
When you call the Concat()extension method, you could very well end up with redundant entries in the fetched result, which could be exactly what you want in some cases. However, in other cases, you might wish to remove duplicate entries in your data. To do so, simply call the Distinct() extension method, as seen here:
static void DisplayConcatNoDups() { List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW" }; List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" }; var carConcat = (from c in myCars select c) .Concat(from c2 in yourCars select c2); // Prints: // Yugo Aztec BMW Saab Aztec. foreach (string s in carConcat.Distinct()) Console.WriteLine(s); }
LINQ queries can also be designed to perform various aggregation operations on the result set. The Count() extension method is one such aggregation example. Other possibilities include obtaining an average, max, min, or sum of values using the Max(), Min(), Average(), or Sum() members of the Enumerable class. Here is a simple example:
static void AggregateOps() { double[] winterTemps = { 2.0, -21.3, 8, -4, 0, 8.2 }; // Various aggregation examples. Console.WriteLine("Max temp: {0}", (from t in winterTemps select t).Max()); Console.WriteLine("Min temp: {0}", (from t in winterTemps select t).Min()); Console.WriteLine("Avarage temp: {0}", (from t in winterTemps select t).Average()); Console.WriteLine("Sum of all temps: {0}", (from t in winterTemps select t).Sum()); }
These examples should give you enough knowledge to feel comfortable with the process of building LINQ query expressions. While there are additional operators you have not yet examined, you will see additional examples later in this text when you learn about related LINQ technologies. To wrap up your first look at LINQ, the remainder of this chapter will dive into the details between the C# LINQ query operators and the underlying object model.
Source Code The FunWithLinqExpressions project can be found under the Chapter 13 subdirectory.